package swpy.fickle {
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import mx.utils.Base64Encoder;

	public class Fickler {
		internal var buffer: ByteArray;
		internal var memo: Array;

		public function Fickler() {
			buffer = new ByteArray();
			memo = new Array();
		};

		private function saveProto():void {
			buffer.writeByte(Opcodes.PROTO);
			buffer.writeByte(Opcodes.DEFAULT_PROTOCOL);
		}

		private function saveFloat(value:Number):void {
			buffer.writeByte(Opcodes.BINFLOAT);
			buffer.endian = Endian.BIG_ENDIAN;
			buffer.writeDouble(value);
			return;
		}
		
		private function saveLong(value:int):void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			if (value >= 0) {
				if (value <= 0xFF) {
					buffer.writeByte(Opcodes.BININT1);
					buffer.writeByte(value);
					return;
				}
				if (value <= 0xFFFF) {
					buffer.writeByte(Opcodes.BININT2);
					buffer.writeShort(value);
					return;
				}
			}
			buffer.writeByte(Opcodes.BININT);
			buffer.writeInt(value);
		}
		
		private function saveStr(value:String):void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			buffer.writeByte(Opcodes.BINUNICODE);
			// Save current position
			var lenPos:uint = buffer.position;
			// Make a placeholder for string length
			buffer.writeInt(0);
			// Save string start position
			var bytesPos:uint = buffer.position;
			buffer.writeMultiByte(value, "utf-8");
			// Calculate string length
			var length:uint = buffer.position - bytesPos;
			// Seek back to string length position
			buffer.position = lenPos;
			buffer.writeInt(length);
			// Seek to the end of the buffer
			buffer.position = buffer.length;
			memoize(value);
		}
		
		private function saveTuple(value:Array):void {
			var len:uint = value.length;
			if (len == 0)
				buffer.writeByte(Opcodes.EMPTY_TUPLE);
			else {
				buffer.writeByte(Opcodes.MARK);
				for (var i:int = 0; i < value.length; i++) {
					save(value[i]);
				}
				if (len <= 3)
					buffer.writeByte(Opcodes.TUPLE1 + len - 1)
				else
					buffer.writeByte(Opcodes.TUPLE);
			}
			memoize(value);
		}
		
		private function saveList(value:Array):void {
			buffer.writeByte(Opcodes.EMPTY_LIST);
			if (value.length > 1)
				buffer.writeByte(Opcodes.MARK);
			for (var i:int = 0; i < value.length; i++) {
				save(value[i]);
			}
			if (value.length == 1)
				buffer.writeByte(Opcodes.APPEND);
			else if (value.length > 1)
				buffer.writeByte(Opcodes.APPENDS);
			memoize(value);
		}
		
		private function saveBytes(value: ByteArray):void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			if (value.length < 256) {
				buffer.writeByte(Opcodes.SHORT_BINBYTES);
				buffer.writeByte(value.length);
			}
			else {
				buffer.writeByte(Opcodes.BINBYTES);
				buffer.writeInt(value.length);
			}
			buffer.writeBytes(value);
			memoize(value);
		}
		
		private function saveDict(value:Object):void {
			buffer.writeByte(Opcodes.EMPTY_DICT);
			// Save this.buffer to a temporary var
			var realBuffer:ByteArray = buffer;
			// Replace this.buffer with a temp ByteArray
			buffer = new ByteArray();
			var attrCount:uint = 0;
			for(var attr:String in value) {
				save(attr);
				save(value[attr]);
				attrCount++;
			}
			if (attrCount > 1)
				realBuffer.writeByte(Opcodes.MARK);
			// Write temp this.buffer to the real this.buffer
			realBuffer.writeBytes(buffer);
			// Swap back temp <-> real this.buffer
			buffer = realBuffer;
			if (attrCount == 1)
				buffer.writeByte(Opcodes.SETITEM);
			else if (attrCount > 1)
				buffer.writeByte(Opcodes.SETITEMS);
			memoize(value);
		}
		
		private function saveGet(i:uint):void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			if (i <= 0xFF) {
				buffer.writeByte(Opcodes.BINGET);
				buffer.writeByte(i);
			}
			else {
				buffer.writeByte(Opcodes.LONG_BINGET);
				buffer.writeInt(i);
			}
		}

		private function savePut(i:uint):void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			if (i <= 0xFF) {
				buffer.writeByte(Opcodes.BINPUT);
				buffer.writeByte(i);
			}
			else {
				buffer.writeByte(Opcodes.LONG_BINPUT);
				buffer.writeInt(i);
			}
		}
		
		private function memoize(obj:*):void {
			if (memo.indexOf(obj) >= 0)
				return;
			var memoLen:uint = memo.length;
			savePut(memoLen);
			memo.push(obj);
		}
								
		private function save(value:*):void {
			// Check the memo
			var i:int = memo.indexOf(value);
			if (i >= 0) {
				saveGet(i);
				return;
			}
			// null or undefined
			if (value == null || value == undefined) {
				buffer.writeByte(Opcodes.NONE);
				return;
			}
			// Number
			if (value is Number) {
				// Float detection
				if ((value.toString().indexOf(".", 0) != -1) || (isNaN(value)) ||
					(value == Number.POSITIVE_INFINITY) ||
					(value == Number.NEGATIVE_INFINITY)) {
					// Float
					saveFloat(value);
					return;
				}
				else {
					// Int
					saveLong(value);
					return;
				}
			}
			// String
			if (value is String) {
				saveStr(value);
				return;
			}
			// Array
			if (value is Array) {
				//saveList(value);
				saveTuple(value);
				return;
			}
			// Boolean true
			if (value == true) {
				buffer.writeByte(Opcodes.NEWTRUE);
				return;
			}
			// Boolean false
			if (value == false) {
				buffer.writeByte(Opcodes.NEWFALSE);
				return;
			}
			// ByteArray
			if( value is ByteArray ) {
				saveBytes(value);
				return;
			}
			// Object
			if (value is Object) {
				saveDict(value);
				return;
			}
			// shouldn't never reach this:
			throw "Unknown value type";
		};

		private function saveStop():void {
			buffer.writeByte(Opcodes.STOP);
		}

		public function toString(): String {
			var resStr:String = "";
			for (var i:uint; i < buffer.length; i++) {
				var char:uint = buffer[i];
				if ((char < 32) || (char > 126)) {
					var hex:String = char.toString(16);
					if (hex.length == 1)
						hex = "0" + hex;
					resStr += "\\x" + hex;
				}
				else
					resStr += String.fromCharCode(char);
			}
			return resStr;
		};

		static public function dumps(value:*):String {
			var fickler: Fickler = new Fickler()
			fickler.saveProto();
			fickler.save(value);
			fickler.saveStop();
			trace(fickler.toString());
			fickler.buffer.compress();
			var encoder:Base64Encoder = new Base64Encoder();
			encoder.encodeBytes(fickler.buffer);
			return encoder.toString();
		}
	}
}